import time
from astral import interpolator

class RemoteObject(object):
    def __init__(self,key,apply_func=None,interp_method=None,prediction_func=None):
        self.key = key
        self.buffered = []  #List of buffered states to interpolate
        self.last_state = {}   #Last state applied
        self.max_buffer = 3   #More buffers will be smoother but take a longer time to get to "now"
        self.buffer_skew = 1   #How fast to go through the buffers. Too fast is jerky, too slow and buffers fill up
        self.buffer_wait = 1   #How many states to buffer before interpolating, should be a lot less than max_buffer
        self.t = 0.0 #Progression between buffered state zero and buffered state one
        
        #Function to take a dictionary of state and apply that to the object
        if not apply_func:
            apply_func = lambda x,y: 1
        self.apply_func = apply_func
        
        #Function to define how we interpolate between two, full, dictionary state representations
        if not interp_method:
            interp_method = {}
        self.interp_method = interp_method
        
        #Function to define how to apply input to our object state for one tick of simulation
        self.prediction_func = prediction_func
        self.use_prediction = False #Set this to true for any objects that should be using prediction
        self.prediction_buffer = {}  #Our predicted states at various times, for correction
        self.max_prediction_buffer = 30
        
        self.values = []
        
        self.init()
    def init(self):
        """Init function to overwrite"""
    def buffer_state(self,state):
        """A new state has come in. Undiff it to previous state and add it to the buffer. 
        If it's the first state, apply it."""
        if state.get("_force_apply",None):
            print "RESET"
            self.buffered[:] = []
        
        #Apply differences
        next_state = {}
        if self.buffered:
            next_state.update(self.buffered[-1])
        next_state.update(state)
        next_state["_time"] = time.time()-0.10
        self.buffered.append(next_state)
        
        while len(self.buffered)>self.max_buffer:
            del self.buffered[0]

        if len(self.buffered)==1:
            self.values = [x for x in state.keys() if not x.startswith("_")]
            self.apply_func(state,self)
    def get_state(self):
        """Similar to the server side get_state, but in this case we base our values
        on the first set of values the server told us about rather than hardcoding 
        the attributres we care about."""
        d = {}
        for k in self.values:
            d[k] = getattr(self,k)
        return d
    def apply_current_state(self):
        """Take current interpolated state out of the buffer
        apply that state to the object using our apply function.
        Only relevant if buffer_wait is greater than 1."""
        if len(self.buffered)<self.buffer_wait:
            return
        a = self.buffered[0]
        b = self.buffered[1]
        state = interpolator.interpolate(a,b,self.t,self.interp_method)
        self.last_state = state
        self.apply_func(state,self)
    def increment_time(self,dt,server_rate):
        if self.use_prediction:
            return
        if len(self.buffered)<2 or len(self.buffered)<self.buffer_wait:
            return
        times = self.buffered[1]["_time"] - self.buffered[0]["_time"]
        rate_skew = float(times)/float(server_rate)   #Adjust to time events actually should have arrived
        if rate_skew < 1:
            rate_skew = 1
        self.t += dt/server_rate*self.buffer_skew*rate_skew
        next = 0
        if self.t>=1:
            self.t = 1
            next = self.t-1.0
        self.apply_current_state()
        if self.t>=1:
            del self.buffered[0]
            self.t = next
    def prediction(self,state,input,t):
        """Starting from state state, at time t0,
        using input data, apply the state that would
        be correct at time t0+t"""
        if not self.prediction_func or not self.use_prediction:
            return
        self.prediction_buffer[t] = {"state":self.get_state(),"input":input,"t":t}
        self.apply_func(state,self)
        self.prediction_func(self,input)
    def correct_prediction(self,t):
        """Compare predicted states with incoming states. If there
        are differences, rerun the prediction up till now"""
        if not self.use_prediction:
            return
        lowest = None
        highest = None
        for b in self.buffered:
            if not lowest or b["_update_count"]<lowest["_update_count"]:
                lowest = b
            if not highest or b["_update_count"]>highest["_update_count"]:
                highest = b
        bad = []
        if highest:
            b = highest
            uc = b["_update_count"]
            if uc in self.prediction_buffer:
                p = self.prediction_buffer[uc]
                bt = {}
                for k in [x for x in b if not x.startswith("_")]:
                    bt[k] = b[k]
                if interpolator.state_diff(bt,p["state"]):
                    input = p["input"][:]
                    input.remove(".p")
                    bad.append({"state":bt,"input":p["input"],"t":uc})
        if bad:
            most_recent = bad[-1]
            state = most_recent["state"]
            for uc in range(most_recent["t"],t+1):
                if uc not in self.prediction_buffer:
                    continue
                input = self.prediction_buffer[uc]["input"]
                self.prediction(state,input,uc)
                state = self.get_state()
                del self.prediction_buffer[uc]
        if not lowest:
            return
        for c in self.prediction_buffer.keys():
            if c<lowest["_update_count"]:
                del self.prediction_buffer[c]
        while len(self.prediction_buffer.keys())>self.max_prediction_buffer:
            del self.prediction_buffer[min(self.prediction_buffer.keys())]
            
def apply_mob_state(state,object):
    """Standard apply function to apply a state to a mob (mobile object)"""
    for k in state:
        if k.startswith("_"):
            continue
        setattr(object,k,state[k])

class Mob(RemoteObject):
    def init(self):
        self.apply_func = apply_mob_state
        self.values = ["x","y"]
